package se.thoka.twt.client.utils;

import java.util.ConcurrentModificationException;
import java.util.NoSuchElementException;

import se.thoka.twt.client.users.UserClientDatastoreManager;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
import com.google.gwt.i18n.client.Messages;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.InvocationException;

/**
 * @author Thobias Karlsson, Thoka
 * @since 9 jan 2010
 */
public class ErrorHandler implements UncaughtExceptionHandler {

	private static final String MSG_LOGIN_FAILED 				= "LOGIN FAILED";
//	private static final String MSG_LOGIN_FAILED_INVALID_STATE 	= "LOGIN FAILED (INVALID STATE)";
	private static final String MSG_RPC_ERROR 					= "RPC ERROR";
	private static final String MSG_SESSION_TIMED_OUT 			= "SESSION TIMED OUT";
	private static final String MSG_UNCAUGHT_EXCEPTION 			= "UNCAUGHT EXCEPTION";
	
	/** The error log service URL */
	protected static final String ERROR_SERVICE_URL = "gwt-error-service";

	/** The internationalization settings */
	public interface Strings extends Messages {
		/** @return Default error title to be displayed when a server request fails, e.g. "Server failure". */
		@DefaultMessage(value = "Server request failed")
		String rpcErrorTitle();

		/** @return Default error message to be displayed when a server request fails. */
		@DefaultMessage(value = "The server request failed.")
		String rpcErrorMessage();

		/** @return Default error title to be displayed when an uncaught exception occurs, e.g. "Application error" */
		@DefaultMessage(value = "Application error")
		String applicationErrorTitle();

		/** @return Default error message to be displayed when an uncaught exception occurs. */
		@DefaultMessage(value = "An uncaught exception was caught in the application.")
		String applicationErrorMessage();

		/** @return Default error title to be displayed when sign-in fails, e.g. "Login failed" */
		@DefaultMessage(value = "Login failed")
		String loginErrorTitle();

		/** @return Default error message to be displayed when sign-in fails due to invalid username or password. */
		@DefaultMessage(value = "The login failed due to invalid username or password")
		String loginErrorMessage();

		/** @return Default error title to be displayed when the users session on the server times out, e.g. "Session timeout!". */
		@DefaultMessage(value = "Session timeout")
		String timeoutErrorTitle();

		/** @return Default error message to be displayed when the users session on the server times out. */
		@DefaultMessage(value = "The session has timed out, you will be logged out.")
		String timeoutErrorMessage();
	}

	private Strings strings;
	private AsyncCallback<Void> serverLogCallback;

	/**
	 * @return <code>false</code> if this instance should <b>not</b> handle uncaught exceptions.
	 */
	public boolean handlesUncaughtExceptions() {
		return true;
	}

	/**
	 * Fired when an error has occurred.
	 * 
	 * @param title The error title, may be <code>null</code>
	 * @param message The error message, may be <code>null</code>
	 * @param caught The caught exception, may be <code>null</code>
	 * @param subsequentCommand A command to be executed after error has been reported, may be <code>null</code>
	 */
	public void onError(String title, String message, Throwable caught, Command subsequentCommand) {
		TWTLogger.log(TWTLogLevel.ERROR, title + " | " + message, caught);

		displayError(title, message, subsequentCommand);
	}

	/**
	 * Fired when login failed.
	 * 
	 * @param caught The caught exception
	 */
	public void onLoginFailed(Throwable caught) {
		Strings strings = getStrings();
		
		String title = strings.loginErrorTitle();
		String message = strings.loginErrorMessage();
		
		TWTLogger.log(MSG_LOGIN_FAILED, caught);

		displayError(title, message, null);
	}

	/**
	 * Fired if the session has timed out.
	 * 
	 * @param caught The caught exception
	 */
	public void onSessionTimedOut(Throwable caught) {
		TWTLogger.log(TWTLogLevel.ERROR, MSG_SESSION_TIMED_OUT, caught);

		Strings strings = getStrings();
		displayError(strings.timeoutErrorTitle(), strings.timeoutErrorMessage(), new Command() {
			@Override
			public void execute() {
				UserClientDatastoreManager<?, ?> userManager = UserClientDatastoreManager.getInstance();
				if (userManager != null) {
					userManager.logoutUser();
				}
			}
		});
	}

	/**
	 * Fired when an RPC error has occurred.
	 * 
	 * @param caught The caught exception
	 * @param message An error message, may be <code>null</code>
	 * @param subsequentCommand A command to be executed after error has been reported, may be <code>null</code>
	 */
	public void onRPCError(Throwable caught, String message, Command subsequentCommand) {
		TWTLogger.log(TWTLogLevel.ERROR, MSG_RPC_ERROR, caught);
		
		Strings strings = getStrings();
		displayError(strings.rpcErrorTitle(), StringUtils.nullOrEmpty(message) ? strings.rpcErrorMessage() : message, subsequentCommand);
	}

	@Override
	public void onUncaughtException(Throwable caught) {
		TWTLogger.log(TWTLogLevel.ERROR, MSG_UNCAUGHT_EXCEPTION, caught);
		
		Strings strings = getStrings();
		onError(strings.applicationErrorTitle(), strings.applicationErrorMessage(), caught, null);
	}
	
	/**
	 * Displays an error message and runs a command after user has "clicked the OK button"
	 * 
	 * @param title The error title, may be <code>null</code>
	 * @param message The error message, may be <code>null</code>
	 * @param subsequentCommand A command to be executed after error has been reported, may be <code>null</code>
	 */
	protected void displayError(String title, String message, Command subsequentCommand) {
		Window.alert(title + " - " + message);
		if (subsequentCommand != null) {
			subsequentCommand.execute();
		}
	}

	/**
	 * Fired when logging to server failed.
	 * 
	 * @param caught The caught exception
	 */
	protected void onServerLogFailed(Throwable caught) {
		TWTLogger.log(TWTLogLevel.WARNING, "Unable to log error on server!", caught);
	}

	/**
	 * Fired when logging to server was successful.
	 */
	protected void onServerLogSuccessed() {
		TWTLogger.log(TWTLogLevel.TRACE, "Error successfully logged on server.");
	}

	/**
	 * Returns the stack trace as a string from the supplied exception
	 * 
	 * @param caught The exception to get stack trace from
	 * @return The stack trace string
	 */
	protected String createStackTrace(Throwable caught) {
		String result = "STACKTRACE:\n";

		try {
			Throwable t = caught;
			while (t != null) {
				String msg = t.getMessage();
				result += t.getClass().getName() + ' ' + (msg != null ? msg : " ") + '\n';
				for (StackTraceElement elem : t.getStackTrace()) {
					result += '\t' + elem.getClassName() + '#' + elem.getMethodName() + '(' + elem.getLineNumber() + ')' + '\n';
				}
				t = t.getCause();
			}
		} catch (Throwable e) {
			result += "Unable to create stack trace from " + caught.getClass().getName();
		}

		return result;
	}

	/**
	 * @return The asynchronous callback that should be used when logging to server log.
	 */
	protected AsyncCallback<Void> getServerLogCallback() {
		if (serverLogCallback == null) {
			serverLogCallback = new AsyncCallback<Void>() {
				@Override
				public void onFailure(Throwable caught) {
					onServerLogFailed(caught);
				}

				@Override
				public void onSuccess(Void result) {
					onServerLogSuccessed();
				}
			};
		}

		return serverLogCallback;
	}

	/**
	 * @return The internationalization settings
	 */
	protected Strings getStrings() {
		if (strings == null) {
			strings = GWT.create(Strings.class);
		}

		return strings;
	}
	
	/**
	 * The exception name of the supplied exception.
	 * 
	 * @param caught The exception
	 * @return The exception name, may result in <code>JavaScriptException</code> in production if unknown.
	 */
	protected String getExceptionClassName(Throwable caught) {
		String result;
		if (caught == null) {
			result = "NULL";
		} else if (caught instanceof ClassCastException) {
			result = "ClassCastException";
		} else if (caught instanceof ConcurrentModificationException) {
			result = "ConcurrentModificationException";
		} else if (caught instanceof IllegalArgumentException) {
			result = "IllegalArgumentException";
		} else if (caught instanceof IllegalStateException) {
			result = "IllegalStateException";
		} else if (caught instanceof IndexOutOfBoundsException) {
			result = "IndexOutOfBoundsException";
		} else if (caught instanceof NegativeArraySizeException) {
			result = "NegativeArraySizeException";
		} else if (caught instanceof NoSuchElementException) {
			result = "NoSuchElementException";
		} else if (caught instanceof NullPointerException) {
			result = "NullPointerException";
		} else if (caught instanceof UnsupportedOperationException) {
			result = "UnsupportedOperationException";
		} else if (caught instanceof InvocationException) {
			result = "InvocationException";
		} else {
			result = caught.getClass().getName();
		}
		
		return result;
	}
}
